/*
  author Sylvain Bertrand <sylvain.bertrand@gmail.com>
  Protected by linux GNU GPLv2
  Copyright 2012-2014
*/
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/i2c.h>
#include <linux/atomic.h>
#include <linux/idr.h>
#include <linux/spinlock.h>

#include <alga/alga.h>
#include <uapi/alga/pixel_fmts.h>
#include <alga/timing.h>
#include <alga/dp.h>
#include <alga/edid.h>
#include <alga/amd/atombios/atb.h>
#include <alga/amd/atombios/dce.h>
#include <uapi/alga/amd/dce6/dce6.h>
#include <alga/amd/dce6/dce6_dev.h>

#define __MOD_C__
#include "dce6.h"
#undef __MOD_C__
#include "hpd.h"
#include "sink.h"
#include "i2c.h"
#include "dpcd.h"
#include "crtc.h"
#include "sysfs.h"
#include "regs.h"

#define WR32(v, of) dce->ddev.wr32(dce->ddev.dev, (v), (of))
#define RR32(of) dce->ddev.rr32(dce->ddev.dev, (of))

static long paths_parse(struct dce6 *dce)
{
	struct atb_dp_path *ps;
	u8 ps_n;
	long r;

	dce->dps_used = 0;
	dce->hpds_used = 0;
	r = atb_dp_paths(dce->ddev.atb, &ps, &ps_n);
	if (r == 0 && ps_n ==0)
		dev_warn(dce->ddev.dev, "dce6:no displayport path found\n");
	else if (r == 0 && ps_n !=0) {
		u32 i;
		for (i = 0; i < ps_n; ++i) {
			struct dp *dp;
			dce->dps_used |= BIT(ps[i].i);

			dp = &dce->dps[ps[i].i];
			dp->atb_dfp = ps[i].dfp;
			dp->atb_aux_i2c_id = ps[i].aux_i2c_id;
			dp->edp = ps[i].edp;
			dp->hpd = ps[i].hpd;
			if (dp->hpd != 0xff)
				dce->hpds_used |= BIT(dp->hpd);

			if (ps[i].uniphy_link_hbr2)
				dp->trans_link_rate_max = DPCD_LINK_BW_5_4;
			else
				dp->trans_link_rate_max = DPCD_LINK_BW_2_7;
		}
		kfree(ps);
	} else if (r == -ATB_ERR)
		return -DCE6_ERR;
	return 0;
}

static void dp_dump(struct dce6 *dce, u8 i)
{
	struct dp *dp;
	const char *s;

	dp = &dce->dps[i];
	if (dp->edp)
		s = "embedded displayport connector";
	else
		s = "displayport connector";
	dev_info(dce->ddev.dev, "\t%s DFP%u atb_aux_i2c_id=0x%02x", s,
					dp->atb_dfp, dp->atb_aux_i2c_id);
		
	s = dp_link_rate_names(dp->trans_link_rate_max);
	dev_info(dce->ddev.dev, "\ttransmitter max link rate %s\n", s);

	if (dp->hpd != 0xff)
		dev_info(dce->ddev.dev, "\thpd%u is used\n",dp->hpd);
	else
		dev_info(dce->ddev.dev, "\tno hpd found\n");
}

static void dps_dump(struct dce6 *dce)
{
	u8 i;

	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		if ((dce->dps_used & BIT(i)) == 0) {
			dev_info(dce->ddev.dev, "dce6:dp%u not used\n", i);
			continue;
		}
		dev_info(dce->ddev.dev, "dce6:dp%u used\n", i);
		dp_dump(dce, i);
	}
}
	
static void dce_dump(struct dce6 *dce)
{
	dps_dump(dce);
}

static long trans_links_init(struct dce6 *dce)
{
	u8 i;

	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		long r;

		if ((dce->dps_used & BIT(i)) == 0)
			continue;		

		r = atb_trans_link_init(dce->ddev.atb, i, dce->dps[i].edp);
		if (r == -ATB_ERR) {
			dev_err(dce->ddev.dev,
				"dce6:failed to init uniphy link for dp%u\n",
									i);
			return -DCE6_ERR;
		}
	}
	return 0;
}

static long dps_sense(struct dce6 *dce)
{
	u8 i;

	dev_info(dce->ddev.dev, "dce6:dps coldplug start:\n");
	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		struct dp *dp;
		u8 sense;
		long r;
		char *s;

		if ((dce->dps_used & BIT(i)) == 0)
			continue;

		dp = &dce->dps[i];

		/* no hpd, presume edp and always connected */
		if (dp->hpd == 0xff) {
			dev_info(dce->ddev.dev, "dce6:\tdp%u has no hpd\n", i);
			r = dp_i2c_adapter_init(dce, i);
			if (r != 0)
				return r;
		
			dp->connected = 1;
			atb_dp_state(dce->ddev.atb, dp->atb_dfp, 1);	
			continue;
		}

		sense = hpd_sense(dce, dp->hpd);
		hpd_polarity_rearm(dce, dp->hpd, sense);

		if (sense){
			s = "connected";
		} else {
			s = "disconnected";
		}
		dev_info(dce->ddev.dev, "dce6:\tdp%u is on hpd%u and is %s\n",
								i, dp->hpd, s);

		r = dp_i2c_adapter_init(dce, i);	
		if (r == -DCE6_ERR)
			return -DCE6_ERR;
		dp->connected = sense;
		atb_dp_state(dce->ddev.atb, dp->atb_dfp, sense);
	}
	return 0;
}

long dp_dpm_off(struct dce6 *dce, u8 i)
{
	long r;

	r = atb_trans_link_off(dce->ddev.atb, i);
	if (r == -ATB_ERR)
		return -DCE6_ERR;

	r = atb_enc_video(dce->ddev.atb, i, dce->dps[i].hpd, 0);
	if (r == -ATB_ERR)
		return -DCE6_ERR;

	if (dce->dps[i].edp) {/* shutdown sink power only for edp */
		r = atb_trans_link_pwr(dce->ddev.atb, i, 0);
		if (r == -ATB_ERR)
			return -DCE6_ERR;
	}

	r = atb_crtc_blank(dce->ddev.atb, i, 1);
	if (r == -ATB_ERR)
		return -DCE6_ERR;

	r = atb_crtc(dce->ddev.atb, i, 0);
	if (r == -ATB_ERR)
		return -DCE6_ERR;

	WR32(0, regs_grph_ena[i]);

	r = atb_crtc_pair_pwr_gate(dce->ddev.atb, i, 1);
	if (r == -ATB_ERR)
		return -DCE6_ERR;
	return 0;
}

static long dps_used_pwr_on(struct dce6 *dce)
{
	u8 i;

	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		long r;
		if ((dce->dps_used & BIT(i)) == 0)
			continue;
		r = atb_trans_link_pwr(dce->ddev.atb, i, 1);
		if (r == -ATB_ERR)
			return -DCE6_ERR;
	}
	usleep_range(1000, 2000); /* dp specs 1.1 */
	return 0;
}

static long dps_used_dpm_off(struct dce6 *dce)
{
	u8 i;
	
	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		long r;
		if ((dce->dps_used & BIT(i)) == 0)
			continue;

		r  = dp_dpm_off(dce, i);
		if (r == -DCE6_ERR)
			return -DCE6_ERR;
	}
	return 0;
}

static long dp_off(struct dce6 *dce, u8 i)
{
	long r;

	r = atb_trans_link_off(dce->ddev.atb, i);
	if (r == -ATB_ERR)
		return -DCE6_ERR;

	r = atb_trans_link_pwr(dce->ddev.atb, i, 0);
	if (r == -ATB_ERR)
		return -DCE6_ERR;

	r = atb_enc_video(dce->ddev.atb, i, dce->dps[i].hpd, 0);
	if (r == -ATB_ERR)
		return -DCE6_ERR;

	r = atb_crtc_blank(dce->ddev.atb, i, 1);
	if (r == -ATB_ERR)
		return -DCE6_ERR;

	r = atb_crtc(dce->ddev.atb, i, 0);
	if (r == -ATB_ERR)
		return -DCE6_ERR;

	WR32(0, regs_grph_ena[i]);

	r = atb_crtc_pair_pwr_gate(dce->ddev.atb, i, 1);
	if (r == -ATB_ERR)
		return -DCE6_ERR;
	return 0;
}

static long dps_unused_off(struct dce6 *dce)
{
	u8 i;

	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		long r;
		if ((dce->dps_used & BIT(i)) != 0)
			continue;

		r = dp_off(dce, i);
		if (r == -DCE6_ERR)
			return -DCE6_ERR;
	}
	return 0;
}

static long dps_connected_dpcd_info(struct dce6 *dce)
{
	u8 i;

	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		long r;
		if ((dce->dps_used & BIT(i)) == 0)
			continue;

		if (!dce->dps[i].connected)
			continue;

		r = dpcd_info(dce, i);
		if (r == -DCE6_ERR)
			return -DCE6_ERR;
	}
	return 0;
}

static void dps_used_active_info(struct dce6 *dce, u8 *active_cnt,
							u8 *active_first)
{
	u8 i;

	*active_cnt = 0;
	*active_first = 0;

	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		if ((dce->dps_used & BIT(i)) == 0)
			continue;

		if (dce->dps[i].active)
			++(*active_cnt);
	}

	if (!*active_cnt)
		return;

	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		if ((dce->dps_used & BIT(i)) == 0)
			continue;

		if (dce->dps[i].active) {
			*active_first = i;
			return;
		}
	}
}

long dce6_sink_mode_set(struct dce6 *dce, u8 i, struct dce_db_fb *db_fb)
{
	long r;
	struct sink_db_fb sink_db_fb;
	struct alga_timing ts[ALGA_TIMINGS_MAX];
	u8 pixel_fmt;
	u8 active_cnt;
	u8 active_first;

	lock(dce);

	if ((dce->dps_used & BIT(i)) == 0) {
		r = -DCE6_ERR;
		goto unlock;
	}

	if (!dce->dps[i].connected) {
		r = -DCE6_ERR;
		goto unlock;
	}

	sink_db_fb.primary = db_fb->primary;
	sink_db_fb.secondary = db_fb->secondary;
	sink_db_fb.pitch = db_fb->pitch;

	/* TODO: edid override for the displayport safe mode */

	if (dce->dps[i].edid){
		struct alga_timing *t;

		r = alga_edid_timings(dce->ddev.dev, dce->dps[i].edid, &ts);
		if (r == -ALGA_ERR) {
			r = -DCE6_ERR;
			goto unlock;
		}

		t = &ts[0];
		while (t->pixel_clk) {
			if (strncmp(&t->mode[0], &db_fb->mode[0],
							sizeof(t->mode)) == 0) {
				sink_db_fb.timing = t;
				break;
			}
			++t;
		}
		if (!t->pixel_clk) {
			r = -DCE6_ERR;
			goto unlock;
		}
	}

	for (pixel_fmt = 1; pixel_fmt < ALGA_PIXEL_FMTS_N; ++pixel_fmt) {
		if (strncmp(alga_pixel_fmts_str[pixel_fmt],
			&db_fb->pixel_fmt[0], sizeof("ARGB2101010")) == 0) {
			sink_db_fb.pixel_fmt = pixel_fmt;
			break;
		}
	}
	if (pixel_fmt == ALGA_PIXEL_FMTS_N) {
		r = -DCE6_ERR;
		goto unlock;
	}

	r = sink_mode_set(dce, i, &sink_db_fb);
	if (r == -DCE6_ERR)
		goto unlock;

	dce->dps[i].active = 1;

	dps_used_active_info(dce, &active_cnt, &active_first);

	/* this is some dynamic power management related notification */
	dce->ddev.dyn_pm_new_display_notify(dce->ddev.dev, active_cnt,
								active_first);
unlock:
	unlock(dce);
	return r;
}
EXPORT_SYMBOL_GPL(dce6_sink_mode_set);

long dce6_dp_dpm(struct dce6 *dce, u8 i)
{
	long r;

	lock(dce);

	if ((dce->dps_used & BIT(i)) == 0) {
		r = -DCE6_ERR;
		goto unlock;
	}

	if (!dce->dps[i].connected) {
		r = -DCE6_ERR;
		goto unlock;
	}
	r = dp_dpm_off(dce, i);
	if (r == -DCE6_ERR)
		goto unlock;

	dce->dps[i].active = 0;

unlock:
	unlock(dce);
	return r;
}
EXPORT_SYMBOL_GPL(dce6_dp_dpm);

#define EDID_SZ_MAX ((255 + 1) * 128)
long dce6_edid(struct dce6 *dce, u8 i, void *edid, u16 edid_sz)
{
	long r;

	lock(dce);

	r = 0;

	if ((dce->dps_used & BIT(i)) == 0) {
		dev_err(dce->ddev.dev, "dce6:dp%u:not in use\n", i);
		r = -DCE6_ERR;
		goto unlock;
	}

	if (!dce->dps[i].connected) {
		dev_err(dce->ddev.dev, "dce6:dp%u:not connected\n", i);
		r = -DCE6_ERR;
		goto unlock;
	}

	if (edid_sz > EDID_SZ_MAX) {
		dev_err(dce->ddev.dev, "dce6:dp%u:edid too big\n", i);
		r = -DCE6_ERR;
		goto unlock;
	}

	if (dce->dps[i].edid != NULL)
		kfree(dce->dps[i].edid);

	dce->dps[i].edid = edid;
	dce->dps[i].edid_sz = edid_sz;

unlock:
	unlock(dce);
	return r;
}
EXPORT_SYMBOL_GPL(dce6_edid);

static void dps_used_off(struct dce6 *dce)
{
	u8 i;

	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		if ((dce->dps_used & BIT(i)) == 0)
			continue;
		
		dp_off(dce, i);
	}
}

static void dps_edid_free(struct dce6 *dce)
{
	u8 i;

	for (i = 0; i < dce->ddev.crtcs_n; ++i)
		if (dce->dps[i].edid != NULL) {
			kfree(dce->dps[i].edid);
			dce->dps[i].edid = NULL;
		}
}

static void edid(struct dce6 *dce, u8 i)
{
	long r;

	/* may have already an allocated edid from before suspend state */
	if (dce->dps[i].edid) {
		kfree(dce->dps[i].edid);
		dce->dps[i].edid = NULL;
	}

	r = alga_i2c_edid_fetch(dce->ddev.dev, &dce->dps[i].i2c_adapter,
							&dce->dps[i].edid);
	if (r <= 0) {
		dev_warn(dce->ddev.dev, "dce6:unable to fetch edid for dp%u\n",
									i);
		return;
	}
	dce->dps[i].edid_sz = r;
}

static void dps_connected_edid(struct dce6 *dce)
{
	u8 i;

	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		if ((dce->dps_used & BIT(i)) == 0)
			continue;

		if (!dce->dps[i].connected)
			continue;

		/* it's not fatal to fail to get the edid */
		edid(dce, i);
	}
}

void dce6_intrs_reset(struct dce6 *dce)
{
	crtcs_intr_reset(dce);
	hpds_polarity_refresh(dce);
}
EXPORT_SYMBOL_GPL(dce6_intrs_reset);

void dce6_vga_off(struct dce6 *dce)
{
	u8 i;

	/* lockout access to vga mem through hdp */
	WR32(VHC_VGA_MEM_DIS, VGA_HDP_CTL);
	WR32(RR32(VGA_RENDER_CTL) & VRC_VGA_VSTATUS_CTL_CLR, VGA_RENDER_CTL);
	for (i = 0; i < dce->ddev.crtcs_n; ++i)
		WR32(0, regs_vga_ctl[i]);
	
}
EXPORT_SYMBOL_GPL(dce6_vga_off);

static long dps_connected_sysfs(struct dce6 *dce)
{
	long r;
	u8 i;

	r = 0;
	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		if ((dce->dps_used & BIT(i)) == 0)
			continue;

		if (!dce->dps[i].connected)
			continue;

		r = sysfs_add(dce, i);
		if (r != 0)
			break;
	}
	return r;
}

static void dps_connected_sysfs_shutdown(struct dce6 *dce)
{
	u8 i;

	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		if ((dce->dps_used & BIT(i)) == 0)
			continue;

		if (!dce->dps[i].connected)
			continue;

		sysfs_remove(dce, i);
	}
}

static void dps_used_pf_lock_init_once(struct dce6 *dce)
{
	u8 i;

	for (i = 0; i < dce->ddev.crtcs_n; ++i) {
		if ((dce->dps_used & BIT(i)) == 0)
			continue;
		spin_lock_init(&dce->dps[i].pf.lock);
	}
}

long dce6_init_once(struct dce6 *dce)
{
	long r;

	mutex_init(&dce->mutex);
	spin_lock_init(&dce->irq.hpds_lock);
	r = paths_parse(dce);
	if (r == -DCE6_ERR)
		return -DCE6_ERR;

	dps_used_pf_lock_init_once(dce);
	dce_dump(dce);
	return 0;
}
EXPORT_SYMBOL_GPL(dce6_init_once);

long dce6_init(struct dce6 *dce, u32 dmif_addr_cfg)
{
	long r;

	lock(dce);

	/* configure the dce memory interface */
	WR32(dmif_addr_cfg, DMIF_ADDR_CFG);
	WR32(dmif_addr_cfg, DMIF_ADDR_CALC);

	crtcs_atb_states_init(dce);

	r = trans_links_init(dce);	
	if (r == -DCE6_ERR)
		return -DCE6_ERR;

	r = atb_crtc_dcpll(dce->ddev.atb);
	if (r == -ATB_ERR)
		return -DCE6_ERR;

	hpds_init(dce);

	r = dps_used_pwr_on(dce);
	if (r == -DCE6_ERR)
		return -DCE6_ERR;
	
	r = dps_used_dpm_off(dce);
	if (r == -DCE6_ERR)
		return -DCE6_ERR;

	r = dps_unused_off(dce);
	if (r == -DCE6_ERR)
		return -DCE6_ERR;

	unlock(dce);
	return 0;
}
EXPORT_SYMBOL_GPL(dce6_init);

long dce6_cold_plug(struct dce6 *dce)
{
	long r;

	lock(dce);

	r = dps_sense(dce);
	if (r == -DCE6_ERR)
		return -DCE6_ERR;

	r = dps_connected_dpcd_info(dce);
	if (r == -DCE6_ERR)
		return -DCE6_ERR;

	dps_connected_edid(dce);

	unlock(dce);
	return 0;
}
EXPORT_SYMBOL_GPL(dce6_cold_plug);

static void off(struct dce6 *dce)
{
	dps_used_off(dce);
	hpds_off(dce);
}

void dce6_edid_free(struct dce6 *dce)
{
	lock(dce);
	dps_edid_free(dce);
	unlock(dce);
}
EXPORT_SYMBOL_GPL(dce6_edid_free);

void dce6_i2c_cleanup(struct dce6 *dce)
{
	lock(dce);
	dp_i2c_cleanup(dce);
	unlock(dce);
}
EXPORT_SYMBOL_GPL(dce6_i2c_cleanup);

void dce6_off(struct dce6 *dce)
{
	lock(dce);
	off(dce);
	unlock(dce);
}	
EXPORT_SYMBOL_GPL(dce6_off);

struct dce6 *dce6_alloc(struct dce6_dev *ddev)
{
	struct dce6 *dce;
	u8 i;

	dce = kzalloc(sizeof(struct dce6), GFP_KERNEL);
	if (!dce)
		return NULL;
	dce->ddev = *ddev;
	for(i = 0; i < CRTCS_N_MAX; ++i) {
		dce->dps[i].dce = dce;
		dce->dps[i].i = i;
	}
	return dce;
}
EXPORT_SYMBOL_GPL(dce6_alloc);

long dce6_sysfs_unregister(struct dce6 *dce)
{
	lock(dce);
	dps_connected_sysfs_shutdown(dce);
	unlock(dce);
	return 0;
}
EXPORT_SYMBOL_GPL(dce6_sysfs_unregister);

void dce6_hpd_dev_registration_lock(struct dce6 *dce)
{
	lock(dce);
	dce->irq.hpd_dev_registration_lock = 1;
	unlock(dce);
}
EXPORT_SYMBOL_GPL(dce6_hpd_dev_registration_lock);

void dce6_hpd_dev_registration_unlock(struct dce6 *dce)
{
	lock(dce);
	dce->irq.hpd_dev_registration_lock = 0;
	unlock(dce);
}
EXPORT_SYMBOL_GPL(dce6_hpd_dev_registration_unlock);

long dce6_sysfs_cold_register(struct dce6 *dce, struct device *parent_char_dev)
{
	long r;
	dce->parent_char_dev = parent_char_dev;

	lock(dce);
	r = dps_connected_sysfs(dce);
	unlock(dce);
	return r;
}
EXPORT_SYMBOL_GPL(dce6_sysfs_cold_register);

static int init(void)
{
	ida_init(&ida);

	class = class_create(THIS_MODULE, "dce6_display");	
	if (IS_ERR_OR_NULL(class)) {
		if (IS_ERR(class))
			return PTR_ERR(class);
		else
			return -ENOMEM;
	}
	return 0;
}

static void cleanup(void)
{
	class_destroy(class);
	ida_destroy(&ida);
}

module_init(init);
module_exit(cleanup);

MODULE_AUTHOR("Sylvain Bertrand <digital.ragnarok@gmail.com>");
MODULE_DESCRIPTION("AMD DCE6");
MODULE_LICENSE("GPL");
